package controller;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.StringTokenizer;
import java.util.Vector;
import rmi.RMIInterface;
import model.exceptions.StreamException;
import model.node.LsmNode;
import model.node.Node;
import model.node.RedNode;
import model.options.Options;
import view.MainWindow;


/**
*	this class is needed to start, pause and stop the simulation and to allow the communication with the server
*/
public class Hypervisor implements ActionListener{
	
	private static java.io.PrintStream outputStreamLog;
	private static java.io.OutputStream osLog;
	private static java.io.PrintStream outputStreamServer;
	private static java.io.OutputStream osServer;
	
	private Integer messages=0;
	private String serverURL;
	private Vector<Node> nodes;
	private int clone;
	private long timeDelay;
	private boolean isRunning = false;
	private boolean pause=false;
	private Object pauseSync = new Object();
	private Object claimSync = new Object();
	private Integer remainingSim;
	private Integer remainingClaims;
	private boolean autoPause = false;
	private boolean allClaimsDone = false;
	private MainWindow mainW;
	private int rand;
	private boolean cloneFind=false;
		
	/**
	* the constructor
	*/
	public Hypervisor(){
		outputStreamServer=System.out;
		outputStreamLog=System.out;
	}

	public MainWindow getMainWindow(){
		return mainW;
	}
	
	/**
	 * This constructor set the OutputStream of model
	 * @param out the OutputStream were all the notification will be printed
	 */
	public Hypervisor(java.io.OutputStream outLog,java.io.OutputStream outCurrentSim){
		osLog=outLog;
		osServer=outCurrentSim;
		outputStreamLog=new java.io.PrintStream(outLog);
		outputStreamServer=new java.io.PrintStream(outCurrentSim);
	}
	
	/**
	 * set the main window of the program to the field mainW
	 * @param mainW the main window
	 */
	public void setMainWindow(MainWindow mainW){
		this.mainW = mainW;
	}

	/**
	 * @return the PrintStream were all the notification about Log will be printed
	 */
	public java.io.PrintStream outLog(){
		return outputStreamLog;
	}
	
	/**
	 * @return the PrintStream were all the notification about running simulation will be printed
	 */
	public java.io.PrintStream outServer(){
		return outputStreamServer;
	}
	
	/**
	 * @return the OutputStream were all the notification about Log will be printed
	 */
	public java.io.OutputStream getOutputStreamLog(){
		return osLog;
	}
	
	/**
	 * @return the OutputStream were all the notification about running simulation will be printed
	 */
	public java.io.OutputStream getOutputStreamServer(){
		return osServer;
	}
	
	/**
	 * Increase the amount of messages of one
	 */
	public void increaseMessages(Node n, int b) {
		synchronized (messages) {
				messages++;
		}
	}
	
	/**
	 * 
	 * @return true if a simulation is running
	 */
	public boolean isRunning(){
		return isRunning;
	};
	
	/**
	 * Decrease the amount of messages
	 * @param number of message to decrease 
	 */
	public void decreaseMessages(int num, Node n, int b) {
		synchronized(messages){
			if(messages > 0){
				messages-=num;
				if(messages <= 0){
					endSim(false,n);
				}
			}
		}
	}
	
	/**
	* it get information from the server, fill the options object and start the simulation
	 * @throws StreamException 
	 * @throws IOException 
	 * @throws UnknownHostException 
	*/
	public void start(String url,int port) {
		try{
			getFromServer(url, port);
		}
		catch(StreamException e){
			outLog().println(e.getMessage());
			return;
		}
		catch(UnknownHostException e){
			outLog().println(e.getMessage());
			return;
		}
		catch(IOException e){
			outLog().println(e.getMessage());
			return;
		}
		
		//Options.getInstance().setProtocol("LSM");
		//Options.getInstance().setNumSim(1);
		remainingSim = Options.getInstance().getNumSim();
		//Options.getInstance().setCommunicationRadius(0.75f);
		//Options.getInstance().setNumNode(5);
		//Options.getInstance().setTotEnergy(200);
		isRunning=true;
		try{
			while(remainingSim > 0 ){
				waitForPause();
				startSim();
				waitingEnd();
				remainingSim-=1;
				synchronized(remainingSim){
					if(remainingSim>=0){
						if(autoPause){
							mainW.getMenu().getButton(0).setEnabled(true);
							mainW.getMenu().getButton(1).setEnabled(false);
							pauseSim();
						}
						String f="clone not found";
						if(cloneFind)
							f="clone FOUND";
						outLog().println("Simulation "+(Options.getInstance().getNumSim()-(remainingSim))+
								" of "+ Options.getInstance().getNumSim() +" complete..."+ f);
						try{
							String res=compileResults(cloneFind);
							cloneFind=false;
							sendToServer(res);
							outServer().println(res);
						} 
						catch (RemoteException e) {
							outLog().println("Problem with connect to server");
							stopSim();
						} 
						catch (MalformedURLException e) {
							outLog().println("URL of server is malformed");
							stopSim();
						} 
						catch (NotBoundException e) {
							outLog().println("Problem with server");
							stopSim();
						}
						
					}
				}
			}
			mainW.getMenu().getButton(0).setEnabled(true);
			mainW.getMenu().getButton(1).setEnabled(false);
			mainW.getMenu().getButton(2).setEnabled(false);
		}
		catch(InterruptedException e){}
	}
	
	
	/**
	* it create all the nodes and the clone
	* and start the communication between the nodes 
	*/
	public void startSim() {
		messages=0;
		nodes = new Vector<Node>();
		
		for (int i=0; i<Options.getInstance().getNumNode(); i++){ 
			nodes.add(createNode(i));
		}
		clone = (int) (Math.random()*nodes.size());
		nodes.add(createNode(nodes.get(clone).getNodeId()));
		boolean c=false;
		for (int i=0; i<nodes.size();i++){
			c = (i==clone || i==nodes.size()-1);
			mainW.getSimulationTelevision().addNode(nodes.get(i).hashCode(), nodes.get(i).getNodeId(),
					nodes.get(i).getX(), nodes.get(i).getY(), c);
		}
		mainW.getSimulationTelevision().start();
		
		allClaimsDone=false;
		remainingClaims=nodes.size();
		if (Options.getInstance().getProtocol().equals("RED")) rand=(int) Math.random()*Integer.MAX_VALUE;
		isRunning = true;
		for (int i=0; i<nodes.size(); i++){
			nodes.get(i).start();
		}
	}
	
	public void manageSim(){
		
	}
	
	/**
	 * used by startSim, create the clone node and upgrade the neighbors list
	 * 
	 * @param id the id of the cloned node
	 * @return a LsmNode or a RedNode according to the Options object
	 */
	private Node createNode(int id){
		Node n;
		if (Options.getInstance().getProtocol().equals("LSM"))			 
			n=new LsmNode(this,(float)Math.random(),(float)Math.random(),id);
		else n=new RedNode(this,(float)Math.random(),(float)Math.random(),id);
		for (int i=0; i<nodes.size();i++){
			if(nodes.get(i).addNeighbor(n))	n.addNeighbor(nodes.get(i));
		}
		return n;
	}
	
	public void claimDone(){
		synchronized(remainingClaims){
			if(remainingClaims > 0)
				remainingClaims--;
			if(remainingClaims==0){
				allClaimsDone=true;
				synchronized (claimSync){claimSync.notifyAll();};
			}
			
		}
	};
	
	public void waitingEnd() throws InterruptedException{
		for(int i=0; i < nodes.size(); i++){
			nodes.get(i).join();
		}
	}
	
	/**
	 * if all Claims are not done wait
	 * 
	 * @throws InterruptedException
	 */
	public void waitForAllClaim() throws InterruptedException {
		if (!allClaimsDone)
			synchronized (claimSync) {
				while (!allClaimsDone)
					claimSync.wait();
			}
	}
	
	
	/**
	* the call on this method pause the simulation if it's running
	*/
	public void pauseSim(){
			synchronized (pauseSync) {
				this.pause = true;
				if (!pause)
					pauseSync.notifyAll();
			}
	}

	/**
	 * if the simulation is pause wait until it resume
	 * 
	 * @throws InterruptedException
	 */
	public void waitForPause() throws InterruptedException {
		if (pause)
			synchronized (pauseSync) {
				while (pause)
					pauseSync.wait();
			}
	}
	
	/**
	 * set the time in millisecond that each node has to wait before it forwards
	 * a received message.
	 * @param timeDelay
	 */
	public void setTimeDelay(long timeDelay){
		this.timeDelay=timeDelay;
	};
	
	/**
	 * Return the delay time in millisecond.
	 * @return Delay time.
	 */
	public long getTimeDelay(){
		return timeDelay;
	};

	/**
	 * if the simulation's status is pause the simulation restart from the previous status
	 */
	public void resumeSim(){
		synchronized(pauseSync){
			if(pause){
				pause=false;
				pauseSync.notifyAll();
			}
		}
	}
	
	/**
	 * stop the simulation and interrupt the nodes
	 */
	public void stopSim(){
			deathToTheNodes();
			remainingSim = -1;
			resumeSim();
	}
	
	/**
	 * when the clone is detected all the nodes are interrupted, the number of remaining simulation is decreased
	 * and the Results are sent to the server 
	 */
	public synchronized void endSim(boolean cloneDetected, Node n){
		if (isRunning){
			deathToTheNodes();
			cloneFind=cloneDetected;
			mainW.getSimulationTelevision().deathNodes();
		}
				
	}
	
	/**
	 * interrupt all the nodes
	 */
	public void deathToTheNodes(){
		if(isRunning){
			for (int i=0; i<nodes.size(); i++)
					nodes.get(i).interrupt();
			isRunning = false;
			mainW.getSimulationTelevision().deathNodes();
		}
	}
	
	/**
	 * compile the Results object with the results of the simulation
	 * @param find true if the clone has been found during the simulation
	 */
	public String compileResults(boolean find){
		float avgSentMsg=0, sdSentMsg=0;
		float avgReceivedMsg=0, sdReceivedMsg=0;
		float avgCheckedSign=0, sdCheckedSign=0;
		float avgConsumedEnergy=0, sdConsumedEnergy=0;
		float avgStoredMsg=0, sdStoredMsg=0;
		int minSentMsg=Integer.MAX_VALUE, maxSentMsg=Integer.MIN_VALUE;
		int minReceivedMsg=Integer.MAX_VALUE, maxReceivedMsg=0;
		int minCheckedSign=Integer.MAX_VALUE, maxCheckedSign=0;
		int minConsumedEnergy=Integer.MAX_VALUE, maxConsumedEnergy=0;
		int minStoredMsg=Integer.MAX_VALUE, maxStoredMsg=0;
		for (int i=0; i<nodes.size(); i++){
			avgSentMsg+=nodes.get(i).getSentMessage()/nodes.size();
			avgReceivedMsg+=nodes.get(i).getReceivedMessage()/nodes.size();
			avgCheckedSign+=nodes.get(i).getCheckedSignature()/nodes.size();
			avgConsumedEnergy+=nodes.get(i).getConsumedEnergy()/nodes.size();
			avgStoredMsg+=nodes.get(i).getStoredMessage()/nodes.size();
			
			if (nodes.get(i).getSentMessage()<minSentMsg){
				minSentMsg=nodes.get(i).getSentMessage();
			}
			if (nodes.get(i).getReceivedMessage()<minReceivedMsg){
				minReceivedMsg=nodes.get(i).getReceivedMessage();
			}
			if (nodes.get(i).getCheckedSignature()<minCheckedSign){
				minCheckedSign=nodes.get(i).getCheckedSignature();
			}
			if (nodes.get(i).getConsumedEnergy()<minConsumedEnergy){
				minConsumedEnergy=nodes.get(i).getConsumedEnergy();
			}
			if (nodes.get(i).getStoredMessage()<minStoredMsg){
				minStoredMsg=nodes.get(i).getStoredMessage();
			}
			
			if (nodes.get(i).getSentMessage()>maxSentMsg){
				maxSentMsg=nodes.get(i).getSentMessage();
			}
			if (nodes.get(i).getReceivedMessage()>maxReceivedMsg){
				maxReceivedMsg=nodes.get(i).getReceivedMessage();
			}
			if (nodes.get(i).getCheckedSignature()>maxCheckedSign){
				maxCheckedSign=nodes.get(i).getCheckedSignature();
			}
			if (nodes.get(i).getConsumedEnergy()>maxConsumedEnergy){
				maxConsumedEnergy=nodes.get(i).getConsumedEnergy();
			}
			if (nodes.get(i).getStoredMessage()>maxStoredMsg){
				maxStoredMsg=nodes.get(i).getStoredMessage();
			}
		}
		for (int i=0; i<nodes.size(); i++){
			sdSentMsg+=Math.pow((nodes.get(i).getSentMessage()-avgSentMsg),2);
			sdReceivedMsg+=Math.pow((nodes.get(i).getReceivedMessage()-avgReceivedMsg),2);
			sdCheckedSign+=Math.pow((nodes.get(i).getCheckedSignature()-avgCheckedSign),2);
			sdConsumedEnergy+=Math.pow((nodes.get(i).getConsumedEnergy()-avgConsumedEnergy),2);
			sdStoredMsg+=Math.pow((nodes.get(i).getStoredMessage()-avgStoredMsg),2);
		}
		sdSentMsg=(float) Math.sqrt(sdSentMsg/nodes.size());
		sdReceivedMsg=(float) Math.sqrt(sdReceivedMsg/nodes.size());
		sdCheckedSign=(float) Math.sqrt(sdCheckedSign/nodes.size());
		sdConsumedEnergy=(float) Math.sqrt(sdConsumedEnergy/nodes.size());
		sdStoredMsg=(float) Math.sqrt(sdStoredMsg/nodes.size());
		
		
		String results = minSentMsg + " " + maxSentMsg + " " + avgSentMsg + " " + sdSentMsg + " ";
		results = results + minReceivedMsg + " " + maxReceivedMsg + " " + avgReceivedMsg + " " + sdReceivedMsg + " ";
		results = results + minCheckedSign + " " + maxCheckedSign + " " + avgCheckedSign + " " + sdCheckedSign + " ";
		results = results + minConsumedEnergy + " " + maxConsumedEnergy + " " + avgConsumedEnergy + " " + sdConsumedEnergy + " ";
		results = results + minStoredMsg + " " + maxStoredMsg + " " + avgStoredMsg + " " + sdStoredMsg;
		if(find)
			results +=" 1";
		else
			results +=" 0";
		
		return results;
		
	}
	
	/**
	 * @return the random number for RED
	 */
	public int getRand(){
		return rand;
	}
	
	/**
	 * send a message to the server to communicate the result of the simulation
	 */
	public void sendToServer(String results) throws RemoteException, MalformedURLException,NotBoundException {
		RMIInterface serverRef = (RMIInterface)Naming.lookup("rmi://" + serverURL + "/myrmiServ");
		serverRef.sendResult(Options.getInstance().getOutput() + " " + results);
	}
	
	/**
	 * get information from the server and fill the Options object
	 */
	public void getFromServer(String url, int port) throws UnknownHostException,IOException, StreamException{
		if (url.startsWith("http://"))
			url = url.substring(7);
		StringTokenizer strtok = new StringTokenizer(url, "/");
		String host = strtok.nextToken();
		String path = url.substring(host.length());
		Socket socket = new Socket(host, port);
		String resourceGet = "GET " + path + " HTTP/1.0\r\n\r\n";
		PrintWriter out = new PrintWriter(socket.getOutputStream());
		out.print(resourceGet);
		out.flush();
		Options.getInstance().load(socket.getInputStream());
	}

	@Override
	public void actionPerformed(ActionEvent ae) {
		
		String action = ae.getActionCommand();
		
		if (action.equals("play")){
			
			
			// upgrade the gui
			mainW.getMenu().getButton(0).setEnabled(false);
			mainW.getMenu().getButton(1).setEnabled(true);
			mainW.getMenu().getButton(2).setEnabled(true);
			mainW.getMenu().setEnableTimePause(false);
			mainW.getForm().editableRMI(false);
			mainW.getForm().editableURL(false);
			setTimeDelay(mainW.getMenu().getPause());
			
			
			if (pause){
				resumeSim();
			}
			else{
				//try {
					mainW.getLogArea().clearLog();
					serverURL = mainW.getForm().getRMI();
					Simulations sim= this.new Simulations();
					sim.start();
					//start(mainW.getForm().getURL(),mainW.getForm().getPort());
				//} catch (UnknownHostException e) {
					// TODO Auto-generated catch block
					//e.printStackTrace();
				//} catch (IOException e) {
					// TODO Auto-generated catch block
					//e.printStackTrace();
				//} catch (StreamException e) {
					// TODO Auto-generated catch block
					//e.printStackTrace();
				//}
			}
			
		}
		else if (action.equals("pause")){
			
			// upgrade the gui
			mainW.getMenu().getButton(0).setEnabled(true);
			mainW.getMenu().getButton(1).setEnabled(false);
			mainW.getMenu().getButton(2).setEnabled(true);
			mainW.getMenu().setEnableTimePause(true);
			
			pauseSim();
			
		}
		else if (action.equals("stop")){
			
			// upgrade the gui
			mainW.getMenu().getButton(0).setEnabled(true);
			mainW.getMenu().getButton(1).setEnabled(false);
			mainW.getMenu().getButton(2).setEnabled(false);
			mainW.getMenu().setEnableTimePause(true);
			mainW.getForm().editableRMI(true);
			mainW.getForm().editableURL(true);
			
			stopSim();
			
		}
		else if (action.equals("autopause")){
			mainW.getMenu().getButton(0).setEnabled(true);
			mainW.getMenu().getButton(1).setEnabled(false);
			mainW.getMenu().getButton(2).setEnabled(true);
			
			autoPause=!autoPause;
		}
	}

	class Simulations extends Thread{
	
		public void run(){
				Hypervisor.this.start(Hypervisor.this.mainW.getForm().getURL(),Hypervisor.this.mainW.getForm().getPort());
		}	
	}
	
}
